package com.bytebuddytest.classvisitor;

import com.util.ClassInfo;
import com.util.FieldInfo;
import com.util.MethodInfo;
import com.util.ParamInfo;
import net.bytebuddy.description.type.TypeDescription;

import java.lang.reflect.Modifier;
import java.util.Collections;
import java.util.HashMap;
import java.util.HashSet;
import java.util.List;
import java.util.Map;
import java.util.Objects;
import java.util.Set;

/**
 * @author yutianhong
 * @version 1.0
 * @since 2023/12/21 14:45
 */
public class ClassSchemaConverter {

    private static final Map<String, String> BASE_CACHE;

    /**
     * for solve circle dependencies problem
     */
    private final Set<String> classSet = new HashSet<>();

    static {
        // init base cache
        Map<String, String> tempMap = new HashMap<>(64);
        // void
        tempMap.put("void", "\"void\"");
        // string
        tempMap.put("java.lang.String", "\"\"");
        // char
        tempMap.put("char", "\"\"");
        tempMap.put("java.lang.Character", "\"\"");
        // byte
        tempMap.put("byte", "0");
        tempMap.put("java.lang.Byte", "0");
        // boolean
        tempMap.put("boolean", "false");
        tempMap.put("java.lang.Boolean", "false");
        tempMap.put("java.util.concurrent.atomic.AtomicBoolean", "false");
        // integer
        tempMap.put("int", "0");
        tempMap.put("short", "0");
        tempMap.put("long", "0");
        tempMap.put("java.util.concurrent.atomic.AtomicInteger", "0");
        tempMap.put("java.util.concurrent.atomic.AtomicLong", "0");
        tempMap.put("java.util.concurrent.atomic.LongAccumulator", "0");
        tempMap.put("java.util.concurrent.atomic.LongAdder", "0");
        tempMap.put("java.lang.Integer", "0");
        tempMap.put("java.lang.Long", "0");
        tempMap.put("java.lang.Short", "0");
        tempMap.put("java.lang.Number", "0");
        tempMap.put("java.math.BigInteger", "0");
        // float
        tempMap.put("float", "0.0");
        tempMap.put("double", "0.0");
        tempMap.put("java.util.concurrent.atomic.DoubleAccumulator", "0.0");
        tempMap.put("java.util.concurrent.atomic.DoubleAdder", "0.0");
        tempMap.put("java.lang.Double", "0.0");
        tempMap.put("java.lang.Float", "0.0");
        tempMap.put("java.math.BigDecimal", "0.0");
        // integer array
        tempMap.put("java.util.concurrent.atomic.AtomicIntegerArray", "[0]");
        tempMap.put("java.util.concurrent.atomic.AtomicLongArray", "[0]");
        // date
        tempMap.put("java.time.LocalDateTime", "\"2023-12-21T19:32:04.673\"");
        tempMap.put("java.time.LocalDate", "\"2023-12-21\"");
        tempMap.put("java.time.LocalTime", "\"19:32:04.674\"");
        tempMap.put("java.util.Date", "1703158842376");
        tempMap.put("java.util.Locale", "\"zh_CN\"");
        tempMap.put("java.time.Month", "\"JANUARY\"");
        tempMap.put("java.time.Instant", "\"2023-12-23T09:03:11.079Z\"");
        tempMap.put("java.time.Duration", "\"PT0S\"");
        // unknown
        tempMap.put("java.lang.Object", "{}");
        BASE_CACHE = Collections.unmodifiableMap(tempMap);
    }

    private final Map<String, StringBuilder> userClassCache = new HashMap<>();
    private final Map<String, ClassInfo> referenceClassInfo;

    public ClassSchemaConverter(Map<String, ClassInfo> referenceClassInfo) {
        this.referenceClassInfo = Objects.requireNonNull(referenceClassInfo);
    }

    /**
     * @param className className
     * @return {@code Map<methodInfo, Map<"in" or "out", schema>> }
     */
    public Map<MethodInfo, Map<String, String>> convertToMethodSchema(String className) {
        Objects.requireNonNull(className);
        Map<MethodInfo, Map<String, String>> ret = new HashMap<>();

        ClassInfo classInfo = referenceClassInfo.get(className);

        Map<String, ParamInfo> typeVariablesTable = new HashMap<>();
        putTypeVariablesDef(typeVariablesTable, classInfo.typeVariablesInfoList);

        List<MethodInfo> methodInfoList = classInfo.getMethodInfoList();
        for (MethodInfo methodInfo : methodInfoList) {
            int modifiers = methodInfo.modifiers;
            // filter method
            if (!Modifier.isPublic(modifiers)
                    || Modifier.isStatic(modifiers) || Modifier.isNative(modifiers) || TypeUtils.isSynthetic(modifiers)) {
                continue;
            }
            if (Objects.equals(classInfo.className, methodInfo.methodName)) {
                continue;   // filter constructor method
            }
            Map<String, String> methodMap = convertMethod(methodInfo, typeVariablesTable);
            ret.put(methodInfo, methodMap);
        }

        return ret;
    }

    private Map<String, String> convertMethod(MethodInfo methodInfo, Map<String, ParamInfo> typeVariablesTable) {
        Map<String, ParamInfo> methodTypeVariablesTable = new HashMap<>(typeVariablesTable);
        putTypeVariablesDef(methodTypeVariablesTable, methodInfo.typeVariablesInfoList);

        ParamInfo outParam = methodInfo.outParam;
        StringBuilder outParamSchema = convertParam(outParam, methodTypeVariablesTable);
        List<ParamInfo> inParam = methodInfo.inParam;
        StringBuilder inParamSchema = new StringBuilder().append("[");
        boolean hasElement = false;
        for (ParamInfo paramInfo : inParam) {
            if (hasElement) {
                inParamSchema.append(",");
            }
            hasElement = true;
            StringBuilder schema = convertParam(paramInfo, methodTypeVariablesTable);
            inParamSchema.append(schema);
        }
        inParamSchema.append("]");

        Map<String, String> methodMap = new HashMap<>(2);
        methodMap.put("out", outParamSchema.toString());
        methodMap.put("in", inParamSchema.toString());
        return methodMap;
    }

    private void putTypeVariablesDef(Map<String, ParamInfo> typeVariablesTable, List<ParamInfo> typeVariablesInfoList) {
        for (ParamInfo paramInfo : typeVariablesInfoList) {
            List<ParamInfo> upperBound = paramInfo.getUpperBound();
            if (upperBound != null && !upperBound.isEmpty()) {
                ParamInfo upperParam = upperBound.get(0);
                if (upperParam != null) {
                    typeVariablesTable.put(paramInfo.className, upperParam);
                }
            }
        }
    }

    private StringBuilder convertParam(ParamInfo paramInfo, Map<String, ParamInfo> typeVariablesTable) {
        StringBuilder stringBuilder = new StringBuilder();
        convertParam0(paramInfo, stringBuilder, typeVariablesTable);
        return stringBuilder;
    }

    private void convertParam0(ParamInfo paramInfo, StringBuilder stringBuilder, Map<String, ParamInfo> typeVariablesTable) {
        if (paramInfo.array) {
            ParamInfo componentType = paramInfo.componentType;
            if (componentType.isPrimitive() &&
                    ("char".equals(componentType.className) || "byte".equals(componentType.className))) {
                // byte[] or char[]
                stringBuilder.append("\"\"");
            } else {
                stringBuilder.append('[');
                convertParam0(componentType, stringBuilder, typeVariablesTable);
                stringBuilder.append(']');
            }
        } else if (paramInfo.wildcard) {
            ParamInfo upperParam = null;
            ParamInfo lowerParam = null;
            // get first upperBound or null
            List<ParamInfo> upperBound = paramInfo.getUpperBound();
            if (upperBound != null && !upperBound.isEmpty()) {
                upperParam = upperBound.get(0);
            }
            // get first lowerBound or null
            List<ParamInfo> lowerBounds = paramInfo.getLowerBounds();
            if (lowerBounds != null && !lowerBounds.isEmpty()) {
                lowerParam = lowerBounds.get(0);
            }
            // use the bound as itself
            if (lowerParam == null && upperParam != null) {
                convertParam0(upperParam, stringBuilder, typeVariablesTable);
            } else if (lowerParam != null) {
                convertParam0(lowerParam, stringBuilder, typeVariablesTable);
            } else {
                stringBuilder.append("{}"); // java.lang.Object as upperBound
            }
        } else if (paramInfo.typeVariable) {
            ParamInfo typeVarParam = typeVariablesTable.get(paramInfo.className);
            if (typeVarParam == null) {
                stringBuilder.append("{}"); // regard as java.lang.Object
            } else {
                convertParam0(typeVarParam, stringBuilder, typeVariablesTable);
            }
        } else {
            convertClass(paramInfo.className, stringBuilder, paramInfo.genericInfoList, typeVariablesTable);
        }
    }

    private void convertClass(String className, StringBuilder builder, List<ParamInfo> genericInfoList, Map<String, ParamInfo> typeVariablesTable) {
        StringBuilder cacheSchema = userClassCache.get(className);
        if (cacheSchema != null) {
            builder.append(cacheSchema);  // hit cache
            return;
        }
        String baseCacheSchema = BASE_CACHE.get(className);
        if (baseCacheSchema != null) {
            builder.append(baseCacheSchema);  // hit cache
            return;
        }

        if (classSet.contains(className)) {
            builder.append("{}"); // circle dependencies
            return;
        }

        ClassInfo classInfo = referenceClassInfo.get(className);
        // improve typeVariablesTable
        if (classInfo != null) {
            classSet.add(className);
            typeVariablesTable = new HashMap<>(typeVariablesTable);
            List<ParamInfo> typeVariablesInfoList = classInfo.typeVariablesInfoList;
            if (genericInfoList == null) {
                genericInfoList = Collections.emptyList();
            }
            for (int i = 0; i < typeVariablesInfoList.size(); i++) {
                if (genericInfoList.size() > i) {
                    typeVariablesTable.put(typeVariablesInfoList.get(i).className, genericInfoList.get(i));
                } else {
                    typeVariablesTable.put(typeVariablesInfoList.get(i).className, null);   // no type arguments, regard as java.lang,Object
                }
            }
        }

        // jdk class with parameters
        StringBuilder classSchema = convertJDKClassWithParameters(className, genericInfoList, typeVariablesTable);
        if (classSchema != null) {
            builder.append(classSchema);
            // not put into cache, because with parameters
            return;
        }

        StringBuilder classBuilder = new StringBuilder();
        if (classInfo == null) {
            // unknown jdk class
            classBuilder.append("{}");
        } else if (TypeUtils.isEnum(classInfo.modifiers, classInfo.superClassName)) {
            // enum
            classBuilder.append("\"\"");
        } else {
            // normal
            classSet.add(className);
            classBuilder.append("{");
            // fields
            List<FieldInfo> fieldInfoList = classInfo.getFieldInfoList();
            for (FieldInfo fieldInfo : fieldInfoList) {
                int modifiers = fieldInfo.modifiers;
                if (Modifier.isStatic(modifiers) || TypeUtils.isSynthetic(modifiers) || Modifier.isTransient(modifiers)) {
                    continue;
                }
                classBuilder.append("\"").append(fieldInfo.fieldName).append("\":");
                convertField(fieldInfo, classBuilder, typeVariablesTable);
                classBuilder.append(",");
            }
            // link super class
            String superClassName = classInfo.getSuperClassName();
            StringBuilder superStringBuilder = new StringBuilder();
            convertClass(superClassName, superStringBuilder, classInfo.getSuperGenericInfoList(), typeVariablesTable);
            if (superStringBuilder.length() >= 2 && superStringBuilder.charAt(0) == '{') {
                // delete '{' and '}'
                superStringBuilder.deleteCharAt(superStringBuilder.length() - 1).deleteCharAt(0);
            }
            if (superStringBuilder.length() == 0 && classBuilder.charAt(classBuilder.length() - 1) == ',') {
                classBuilder.deleteCharAt(classBuilder.length() - 1);   // delete ','
            }
            classBuilder.append(superStringBuilder);
            classBuilder.append("}");
        }
        builder.append(classBuilder);
        if (classInfo == null
                || classInfo.typeVariablesInfoList == null
                || classInfo.typeVariablesInfoList.isEmpty()) {
            // unknown or without parameters
            userClassCache.put(className, classBuilder);
        }
        classSet.remove(className);
    }

    private void convertField(FieldInfo fieldInfo, StringBuilder stringBuilder, Map<String, ParamInfo> typeVariablesTable) {
        if (fieldInfo.array) {
            ParamInfo componentType = fieldInfo.componentType;
            if (componentType.isPrimitive() &&
                    ("char".equals(componentType.className) || "byte".equals(componentType.className))) {
                // byte[] or char[]
                stringBuilder.append("\"\"");
            } else {
                stringBuilder.append('[');
                convertParam0(componentType, stringBuilder, typeVariablesTable);
                stringBuilder.append(']');
            }
        } else if (fieldInfo.typeVariable) {
            ParamInfo typeVarParam = typeVariablesTable.get(fieldInfo.className);
            if (typeVarParam == null) {
                stringBuilder.append("{}"); // regard as java.lang.Object
            } else {
                convertParam0(typeVarParam, stringBuilder, typeVariablesTable);
            }
        } else {
            // normal object field
            convertClass(fieldInfo.className, stringBuilder, fieldInfo.genericInfoList, typeVariablesTable);
        }
    }

    /**
     * @return the transferred class schema. if className can't be transferred, null will be return.
     */
    private StringBuilder convertJDKClassWithParameters(String className, List<ParamInfo> genericInfoList, Map<String, ParamInfo> typeVariablesTable) {
        StringBuilder stringBuilder = new StringBuilder();
        ClassInfo classInfo = referenceClassInfo.get(className);
        TypeDescription.Generic typeDefinition = getTypeDefinitions(className, classInfo);

        // special class with parameters
        if ("java.util.concurrent.atomic.AtomicReferenceArray".equals(className)
                || TypeUtils.implementsInterface(typeDefinition, "java.lang.Iterable")
                || TypeUtils.implementsInterface(typeDefinition, "java.util.Iterator")) {
            stringBuilder.append("[");
            ParamInfo paramInfo = !genericInfoList.isEmpty() ? genericInfoList.get(0) : null;
            if (paramInfo != null) {
                convertParam0(paramInfo, stringBuilder, typeVariablesTable);
            }
            stringBuilder.append("]");
        } else if (TypeUtils.implementsInterface(typeDefinition, "java.util.Map")) {
            stringBuilder.append("{");
            // key must be string
            ParamInfo paramInfo0 = !genericInfoList.isEmpty() ? genericInfoList.get(0) : null;
            if (paramInfo0 != null) {
                StringBuilder keyBuilder = new StringBuilder();
                convertParam0(paramInfo0, keyBuilder, typeVariablesTable);
                if (keyBuilder.charAt(0) == '"' && keyBuilder.charAt(keyBuilder.length() - 1) == '"') {
                    stringBuilder.append(keyBuilder);
                } else {
                    stringBuilder.append("\"").append(keyBuilder).append("\"");
                }
            } else {
                stringBuilder.append("\"\"");
            }
            stringBuilder.append(":");
            ParamInfo paramInfo1 = genericInfoList.size() >= 2 ? genericInfoList.get(1) : null;
            if (paramInfo1 != null) {
                convertParam0(paramInfo1, stringBuilder, typeVariablesTable);
            } else {
                stringBuilder.append("null");
            }
            stringBuilder.append("}");
        } else if ("java.util.concurrent.atomic.AtomicReference".equals(className)) {
            ParamInfo paramInfo = !genericInfoList.isEmpty() ? genericInfoList.get(0) : null;
            if (paramInfo != null) {
                convertParam0(paramInfo, stringBuilder, typeVariablesTable);
            } else {
                stringBuilder.append("null");
            }
        } else {
            // not hit, return null
            stringBuilder = null;
        }
        return stringBuilder;
    }

    private static TypeDescription.Generic getTypeDefinitions(String className, ClassInfo classInfo) {
        TypeDescription.Generic typeDefinition = null;
        if (classInfo != null) {
            typeDefinition = classInfo.typeDefinition;
        } else if (TypeUtils.loadableByExtClassLoader(className)) {
            typeDefinition = TypeUtils.getTypePool().describe(className).resolve().asGenericType();
        }
        return typeDefinition;
    }

}
